Skip to content

feat: implement raise_dispute and the escrow freeze_for_dispute hook - #49

Merged
JamesVictor-O merged 1 commit into
Ads-Bazaar:mainfrom
B-Hands:feat/raise-dispute
Jul 25, 2026
Merged

feat: implement raise_dispute and the escrow freeze_for_dispute hook#49
JamesVictor-O merged 1 commit into
Ads-Bazaar:mainfrom
B-Hands:feat/raise-dispute

Conversation

@Anuoluwapo25

@Anuoluwapo25 Anuoluwapo25 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

What this does

Turns dispute-resolution::raise_dispute from a todo!() panic into a working flow, and implements the campaign-escrow::freeze_for_dispute hook it depends on. raise_dispute is the entry point for the whole arbitration flow — nothing else in the contract could be exercised until a Dispute record actually got created.

raise_dispute now persists a Dispute under a fresh DisputeId, publishes DisputeRaised, and freezes the contested payout in escrow via a real cross-contract call so it can't be claimed mid-review.

Design decisions settled here

Both are documented at the call site.

  • Who may raise — either side of the payout: the creator, or the campaign's business. Business ownership lives in campaign-escrow, so it's verified via a cross-contract get_campaign_business read rather than trusted from the caller. A stranger naming themselves creator is rejected because escrow refuses to freeze a (campaign_id, creator) pair with no settleable application.
  • Time window — none, deliberately. The bound that actually protects funds is "before claim", which escrow already enforces by refusing to freeze an already-Paid application. This contract can't see proof-submission timestamps anyway, so a day-based deadline would only add a second, weaker rule.
  • Repeat disputes — one open dispute per (campaign_id, creator); a second attempt fails with DisputeAlreadyRaised.

Escrow side

freeze_for_dispute is a per-application freeze (not a campaign-wide halt) so one contested creator doesn't stall payouts to everyone else on the same brief. It's authenticated to the configured dispute_contract. A new frozen flag on Application blocks claim_payment, submit_proof, approve_submission and reject_submission — the last three because the proof is the evidence the arbiter reviews. The existing admin resolve_dispute clears the flag on settlement.

To keep escrow's contract exports out of the dispute-resolution wasm, the dispute contract declares a hand-written #[contractclient] for the two escrow methods it calls rather than depending on the escrow crate at build time; escrow is a dev-dependency only, for the cross-contract tests.

Not in scope

resolve_dispute_payout remains todo!() — its doc comment now notes it must clear frozen when it lands, the way the admin resolve_dispute shortcut already does.

Note for reviewers

This adds a frozen: bool field to the Application struct, changing its stored layout. Fine on a fresh deploy; a live network with existing escrow state would need a migration. Assumed fresh deploy given the repo is a pre-launch scaffold.

Testing

Mirrors .github/workflows/ci.yml — all pass locally:

  • cargo fmt --all -- --check — clean
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo test --workspace — 67 + 17 pass (was 67 + 7)
  • cargo build --workspace --target wasm32v1-none --release — exit 0

The old raise_dispute_is_not_yet_implemented #[should_panic] test is replaced by an 11-test test_raise_dispute module (incrementing IDs, stored-field correctness, business-raised disputes, unauthorized-caller rejection, escrow freeze, duplicate rejection, post-deadline raising), plus 11 new escrow-side freeze_for_dispute tests.

closes #40

Turns the dispute-resolution contract's entry point from a todo!() panic
into a working flow, plus the campaign-escrow hook it depends on.

dispute-resolution::raise_dispute now persists a Dispute record under a
fresh DisputeId, publishes DisputeRaised, and freezes the contested payout
in escrow so it can't be claimed mid-review.

Design decisions settled in this PR (documented at the call site):

- Who may raise: either side of the payout — the creator, or the campaign's
  business. Business ownership lives in campaign-escrow, so it's verified via
  a cross-contract get_campaign_business read rather than trusted from the
  caller. A stranger naming themselves creator is rejected because escrow
  refuses to freeze a campaign/creator pair with no settleable application.
- Time window: none. The bound that protects funds is "before claim", which
  escrow already enforces by refusing to freeze an already-Paid application.
  This contract can't see proof-submission timestamps anyway.
- Repeat disputes: one open dispute per (campaign_id, creator); a second
  attempt fails with DisputeAlreadyRaised.

campaign-escrow::freeze_for_dispute is implemented as a per-application
freeze (not a campaign-wide halt) so one contested creator doesn't stall
payouts to everyone else on the same brief. It's authenticated to the
configured dispute_contract, and the new `frozen` flag blocks claim_payment,
submit_proof, approve_submission and reject_submission — the last three
because the proof is the evidence the arbiter reviews. resolve_dispute
clears the flag on settlement.

The dispute contract declares a hand-written #[contractclient] for the two
escrow methods it calls rather than depending on the escrow crate at build
time, so escrow's contract exports don't leak into the dispute wasm; escrow
is a dev-dependency only, for the cross-contract tests.

resolve_dispute_payout remains todo!() — see its doc comment; it must clear
`frozen` when it lands, as the admin resolve_dispute shortcut already does.
@JamesVictor-O
JamesVictor-O merged commit 7d6c7c9 into Ads-Bazaar:main Jul 25, 2026
@JamesVictor-O

Copy link
Copy Markdown
Contributor

Merged, nice work — this is a solid piece of the arbitration flow, and the design write-up in the description made review straightforward. The per-application freeze (rather than a campaign-wide halt) and the require_auth-on-the-callee cross-contract auth pattern are both exactly right.

Your branch was cut before #31 and #34 landed on main, which caused one real conflict: PayoutFrozen independently claimed error discriminant 24, the same slot #31 had already given to InvalidCreatorCount. Since I don't have write access to push to your fork branch, I resolved it directly — bumped PayoutFrozen to 25, no other change — and merged via a regular merge commit rather than GitHub's squash button, which is why the merge commit shows both of us as authors. Re-verified after resolving: fmt, full workspace build, all 97 tests (80 escrow + 17 dispute-resolution), clippy -D warnings, and the wasm32v1-none release build all clean. CI is green on main: https://github.com/Ads-Bazaar/ads-bazaar-contract/actions/runs/30161534915

Also confirmed #34's propose_admin/accept_admin (merged after your branch was cut) survived the merge intact alongside your changes — no other regressions.

JamesVictor-O pushed a commit that referenced this pull request Jul 25, 2026
…50)

storage::add_campaign_applicant rewrote an ever-growing Vec<Address> on every apply_to_campaign call just to answer a yes/no question in has_campaign_applicants. Replaces it with a u32 counter under DataKey::ApplicantCount, matching the existing approved_count pattern on Campaign, so applying costs O(1) storage-writes regardless of how many creators already applied.

Adds a regression test that applies 200+ creators and asserts (via env.cost_estimate()) that the storage write cost of a later apply matches an early one, plus confirms update_campaign_metadata's lock-after-first-application behavior from #38 is unaffected.

Closes #43

Verified locally in an isolated worktree before merge:
- cargo fmt --all -- --check
- cargo build --workspace
- cargo test --workspace (64 tests)
- cargo clippy --workspace --all-targets -- -D warnings
- cargo build --workspace --target wasm32v1-none --release
- Confirmed via git merge-tree that the merge combines cleanly with #34 and #49, which landed after this branch was cut.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: implement dispute-resolution::raise_dispute()

2 participants